Real-time Video Comparison:
Touch-Friendly Before/After Slider in Vanilla JS
Draft
Table of Contents
This post contains code for a simple responsive comparison slider that users can drag to reveal the difference between two videos in real-time. The slider is designed to work on both desktop and mobile devices. It includes basic functionality to help keep the videos synchronized.
This is a fork of this picture comparison slider. The following code is distributed under the MIT License.
Live Demo #
Code #
HTML #
<div class="slider-wrapper">
<div class="Slider">
<div id="one" class="bal-container">
<div class="bal-after">
<video autoplay loop muted playsinline>
<source src="videos/FBL.mp4" type="video/mp4" />
</video>
<div class="bal-afterPosition afterLabel">Video 2</div>
</div>
<div class="bal-before">
<div class="bal-before-inset">
<video autoplay loop muted playsinline>
<source src="videos/no_FBL.mp4" type="video/mp4" />
</video>
<div class="bal-beforePosition beforeLabel">Video 1</div>
</div>
</div>
<div class="bal-handle">
<span class="handle-left-arrow"></span>
<span class="handle-right-arrow"></span>
</div>
</div>
</div>
</div>
CSS #
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.slider-wrapper {
margin: 0 auto;
padding: 0px;
margin-bottom: 25px;
border-radius: 10px;
overflow: hidden;
width: 100%;
max-width: 100%;
}
.Slider {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: space-around;
align-items: stretch;
align-content: stretch;
height: 0;
padding-top: 56.25%;
position: relative;
}
.bal-container {
margin: 0px 0;
}
.bal-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: grab;
overflow: hidden;
font-family: inherit;
font-weight: inherit;
}
.bal-after,
.bal-before {
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 15;
overflow: hidden;
}
.bal-before-inset {
position: absolute;
top: 0;
bottom: 0;
left: 0;
}
.bal-after video,
.bal-before video {
object-fit: cover;
position: absolute;
width: 100%;
height: 100%;
object-position: 50% 50%;
margin-top: 0;
margin-bottom: 0;
left: 0;
-webkit-user-select: none;
user-select: none;
pointer-events: none;
}
.bal-beforePosition,
.bal-afterPosition {
background: #1e293b;
color: #e5e9f0;
pointer-events: none;
border-radius: 0.2rem;
padding: 2px 10px;
}
.beforeLabel {
position: absolute;
top: 0;
left: 0;
margin: 1rem;
font-size: 1.4em;
user-select: none;
}
.afterLabel {
position: absolute;
top: 0;
right: 0;
margin: 1rem;
font-size: 1.4em;
user-select: none;
}
.bal-handle {
height: 41px;
width: 41px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -20px;
margin-top: -21px;
border: 3px solid #e5e9f0;
border-radius: 1000px;
z-index: 20;
pointer-events: none;
box-shadow: 0 0 10px rgb(12, 12, 12);
}
.handle-left-arrow,
.handle-right-arrow {
width: 0;
height: 0;
border: 6px inset transparent;
position: absolute;
top: 50%;
margin-top: -6px;
scale: 1.3;
}
.handle-left-arrow {
border-right: 6px solid #e5e9f0;
left: 50%;
margin-left: -17px;
}
.handle-right-arrow {
border-left: 6px solid #e5e9f0;
right: 50%;
margin-right: -17px;
}
.bal-handle::before {
bottom: 50%;
margin-bottom: 20px;
box-shadow: 0 0 10px #1e293b;
}
.bal-handle::after {
top: 50%;
margin-top: 20.5px;
box-shadow: 0 0 5px #1e293b;
}
.bal-handle::before,
.bal-handle::after {
content: " ";
display: block;
width: 3px;
background: #e5e9f0;
height: 480px;
position: absolute;
left: 50%;
margin-left: -1.5px;
}
</style>
JS #
<script>
class BeforeAfter {
constructor(enteryObject) {
const beforeAfterContainer = document.querySelector(enteryObject.id);
const before = beforeAfterContainer.querySelector(".bal-before");
const beforeVideo = before.querySelector("video");
const afterVideo = beforeAfterContainer.querySelector(".bal-after video");
const beforeText = beforeAfterContainer.querySelector(
".bal-beforePosition"
);
const afterText = beforeAfterContainer.querySelector(
".bal-afterPosition"
);
const handle = beforeAfterContainer.querySelector(".bal-handle");
let widthChange = 0;
// Synchronize videos
const syncVideos = () => {
beforeVideo.currentTime = 0;
afterVideo.currentTime = 0;
const playVideos = () => {
beforeVideo.play();
afterVideo.play();
};
if (beforeVideo.readyState >= 2 && afterVideo.readyState >= 2) {
playVideos();
} else {
beforeVideo.addEventListener("loadedmetadata", playVideos, { once: true });
afterVideo.addEventListener("loadedmetadata", playVideos, { once: true });
}
};
syncVideos();
beforeAfterContainer
.querySelector(".bal-before-inset")
.setAttribute(
"style",
"width: " + beforeAfterContainer.offsetWidth + "px;"
);
window.onresize = function () {
beforeAfterContainer
.querySelector(".bal-before-inset")
.setAttribute(
"style",
"width: " + beforeAfterContainer.offsetWidth + "px;"
);
};
before.setAttribute("style", "width: 50%;");
handle.setAttribute("style", "left: 50%;");
// Touch screen event listener
beforeAfterContainer.addEventListener("touchstart", (e) => {
beforeAfterContainer.addEventListener("touchmove", (e2) => {
let containerWidth = beforeAfterContainer.offsetWidth;
let currentPoint = e2.changedTouches[0].clientX;
let startOfDiv = beforeAfterContainer.offsetLeft;
let modifiedCurrentPoint = currentPoint - startOfDiv;
if (
modifiedCurrentPoint > 10 &&
modifiedCurrentPoint < beforeAfterContainer.offsetWidth - 10
) {
let newWidth = (modifiedCurrentPoint * 100) / containerWidth;
before.setAttribute("style", "width:" + newWidth + "%;");
afterText.setAttribute("style", "z-index: 1;");
handle.setAttribute("style", "left:" + newWidth + "%;");
}
});
});
// Mouse move event listener
beforeAfterContainer.addEventListener("mousemove", (e) => {
let containerWidth = beforeAfterContainer.offsetWidth;
widthChange = e.offsetX;
let newWidth = (widthChange * 100) / containerWidth;
if (
e.offsetX > 10 &&
e.offsetX < beforeAfterContainer.offsetWidth - 10
) {
before.setAttribute("style", "width:" + newWidth + "%;");
afterText.setAttribute("style", "z-index:" + "1;");
handle.setAttribute("style", "left:" + newWidth + "%;");
}
});
}
}
// Initialize BeforeAfter for the container
new BeforeAfter({
id: "#one",
});
</script>